Skip to content

feat(share_plus)!: SharePlus refactor #3404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Apr 18, 2025
Merged

Conversation

miquelbeltran
Copy link
Member

@miquelbeltran miquelbeltran commented Dec 18, 2024

Description

This PR contains several improvements on the share_plus package:

  • Unified the 3 static methods into a single non-static method.
  • Access to SharePlus class via instance singleton, rather than using static methods.
  • Allow to mock SharePlatform for testing.
  • Consolidated the use of subject and title.
  • Wrapped all share parameters into the ShareParams class.
  • Unified native share implementations into a single method (when possible).
  • Added mailToFallbackEnabled to disable web mailTo failback (enabled by default)
  • downloadFallbackEnabled is no longer a static setting but part of the ShareParams
  • Sharing uri is now supported on all platforms by sharing the URI as plain text

Not a breaking change, since the old API is still compatible, only it has been deprecated.
But we should consider bumping the version of share_plus to a major release nevertheless, since the deprecation will break CI/CD lint analysis.

TODO:

  • Implement Linux
  • Implement macOS
  • Implement Android
  • Implement iOS
  • Implement Windows Native
  • Implement Windows Fallback
  • Implement Web
  • Implement new tests
  • Document public method

Related Issues

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I titled the PR using Conventional Commits.
  • I did not modify the CHANGELOG.md nor the plugin version in pubspec.yaml files.
  • All existing and new tests are passing.
  • The analyzer (flutter analyze) does not report any problems on my PR.

Breaking Change

Does your PR require plugin users to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (please indicate that with a ! in the title as explained in Conventional Commits).
  • No, this is not a breaking change.

@miquelbeltran
Copy link
Member Author

One last big piece missing is the Windows refactor, which I plan to do tomorrow or so.

Then I'll write up a migration guide on the README.md.

The API and functionality is still backwards compatible, but just getting deprecated warnings. The old Share class will be removed eventually.

The subject and title functionality has been consolidated:

  • title is used everywhere where the value is used in the share sheet as title.
  • subject is used for email subjects
    • on iOS and macOS, to keep compatibility and avoid having a breaking change, it will use the subject if title is not provided, for the share sheet title.

@miquelbeltran miquelbeltran marked this pull request as ready for review December 20, 2024 09:45
@miquelbeltran miquelbeltran changed the title feat(share_plus): SharePlus refactor (WIP) feat(share_plus): SharePlus refactor Dec 20, 2024
@miquelbeltran
Copy link
Member Author

Hey @StanleyCocos I am very sorry I wanted to have this done during the winter break, and then I got sidetracked. Can I ask you a favor? Could you take a look and give me a review?

@miquelbeltran miquelbeltran self-assigned this Apr 1, 2025
@miquelbeltran miquelbeltran requested a review from Copilot April 1, 2025 11:44
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the share_plus package by unifying sharing methods into a single non-static approach using a singleton instance and the ShareParams class.

  • Consolidated sharing methods into a single instance method call.
  • Updated examples and documentation in the README.md to reflect the new API usage.
  • Added a migration guide for converting from the deprecated static API.
Files not reviewed (19)
  • packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/MethodCallHandler.kt: Language not supported
  • packages/share_plus/share_plus/android/src/main/kotlin/dev/fluttercommunity/plus/share/Share.kt: Language not supported
  • packages/share_plus/share_plus/example/ios/Runner/AppDelegate.swift: Language not supported
  • packages/share_plus/share_plus/example/lib/main.dart: Language not supported
  • packages/share_plus/share_plus/example/windows/flutter/CMakeLists.txt: Language not supported
  • packages/share_plus/share_plus/example/windows/runner/Runner.rc: Language not supported
  • packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m: Language not supported
  • packages/share_plus/share_plus/lib/share_plus.dart: Language not supported
  • packages/share_plus/share_plus/lib/src/share_plus_linux.dart: Language not supported
  • packages/share_plus/share_plus/lib/src/share_plus_web.dart: Language not supported
  • packages/share_plus/share_plus/lib/src/share_plus_windows.dart: Language not supported
  • packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift: Language not supported
  • packages/share_plus/share_plus/test/share_plus_linux_test.dart: Language not supported
  • packages/share_plus/share_plus/test/share_plus_test.dart: Language not supported
  • packages/share_plus/share_plus/test/share_plus_windows_test.dart: Language not supported
  • packages/share_plus/share_plus/windows/share_plus_plugin.cpp: Language not supported
  • packages/share_plus/share_plus/windows/share_plus_windows_plugin.h: Language not supported
  • packages/share_plus/share_plus_platform_interface/lib/method_channel/method_channel_share.dart: Language not supported
  • packages/share_plus/share_plus_platform_interface/lib/platform_interface/share_plus_platform.dart: Language not supported

@miquelbeltran miquelbeltran changed the title feat(share_plus): SharePlus refactor feat(share_plus)!: SharePlus refactor Apr 4, 2025
@miquelbeltran
Copy link
Member Author

Fixing the analysis errors, and main is merged in as well.

I am going to mark this as a breaking change, although technically it is not, using this package will cause deprecated warnings to appear and the change is big enough that it deserves it.

I am setting myself the goal to merge this by April 18th (two weeks from now). Please, @StanleyCocos and anyone else interested, I'd appreciate a lot to have a review of the changes.

@StanleyCocos
Copy link
Contributor

Of course, I’ll try to take a look and offer some suggestions from my side. Things have been quite busy for me recently, but as you mentioned, the merge is in two weeks — I’ll do my best to wrap this up before then. Thanks again!

@StanleyCocos
Copy link
Contributor

Can we add some test cases specifically for sharing to ensure the platform-side code works correctly?
https://github.com/fluttercommunity/plus_plugins/blob/main/packages/share_plus/share_plus/example/ios/RunnerTests/RunnerTests.swift

@miquelbeltran
Copy link
Member Author

Can we add some test cases specifically for sharing to ensure the platform-side code works correctly? main/packages/share_plus/share_plus/example/ios/RunnerTests/RunnerTests.swift

I'd definitely want to add more native tests, not sure in this PR or later on.

Thanks a lot for the review @StanleyCocos ! I'm still going to wait until 18th to merge, in case any new suggestions come.

@StanleyCocos
Copy link
Contributor

@miquelbeltran
Thank you.
I think I may not have expressed myself clearly in the thoughts I shared earlier. I’ve added some clarification—could you please take another look?

@miquelbeltran
Copy link
Member Author

@StanleyCocos I can't find your new comments

/// Throws [ArgumentError] if [ShareParams] are invalid.
///
/// Throws other types of exceptions if the share method fails.
Future<ShareResult> share(ShareParams params) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make the share method static. Right now, we can only use SharePlus.instance, but I feel that using SharePlus.share would be more concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope :) that was the whole point of the refactor, to make SharePlus instantiable and not use static methods. We had reports that the old Share class was hard to mock/fake because of that so we are moving to this pattern where you can either create an instance of the class or use SharePlus.instance, which I have seen in other packages e.g. Firebase.

return;
}
// Check if text provided is valid
if (shareText && shareText.length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title parameter is not supposed to be mandatory. If it’s set to an empty string, the feature might not work properly. For optional features like this, I don’t think it should be validated this way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await SharePlus.instance.share(
      ShareParams(
        text: 'test',
        subject: '',
        title: '',
      ),
    );

When I share like this, the following exception occurs.
If the title is an empty string (''), can we just ignore it?

Unhandled Exception: PlatformException(error, Non-empty title expected, null, null)
#0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:648:7)
#1 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:334:18)

#2 MethodChannelShare.share (package:share_plus_platform_interface/method_channel/method_channel_share.dart:25:20)

#3 DemoAppState._onShareWithResult (package:share_plus_example/main.dart:218:4)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this comment was for line 288, as this if-condition is checking text, not title.

Title is not mandatory indeed. This could probably be done indeed, although I am not sure how the UI would behave with an empty string. I am fine to do this change afterward checking how the UI works in that case, I am not sure also if there is a usecase where the title should be an empty string as opposed to ignore it.

///
/// * Supported platforms: iOS, Android
/// Fallsback to sharing the URI as text on other platforms.
final Uri? uri;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I see, when a uri is provided, the text becomes ineffective because uri is checked first. So I’m wondering if we could define a priority—for example, if a uri is passed, then text is ignored—instead of making the two parameters mutually exclusive.

Copy link
Member Author

@miquelbeltran miquelbeltran Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

if (params.uri != null && params.text != null) {
throw ArgumentError('uri and text cannot be provided at the same time');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we define a priority? For example, if there’s a Uri, we can ignore the text. At the very least, let the user share successfully. Right now, directly aborting like this feels a bit too arbitrary.

SharePlus.instance.share(
      ShareParams(
        text: 'test',
        subject: '',
        title: '',
        uri: Uri.tryParse('https://google.com')
      ),
    );

Unhandled Exception: Invalid argument(s): uri and text cannot be provided at the same time
E/flutter (17265): #0 SharePlus.share (package:share_plus/share_plus.dart:78:7)
E/flutter (17265): #1 DemoAppState._onShareWithResult (package:share_plus_example/main.dart:218:29)
E/flutter (17265): #2 DemoAppState.build.. (package:share_plus_example/main.dart:167:33)
E/flutter (17265): #3 _InkResponseState.handleTap (package:flutter/src/material/ink_well.dart:1170:21)
E/flutter (17265): #4 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:351:24)
E/flutter (17265): #5 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:656:11)
E/flutter (17265): #6 BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:313:5)
E/flutter (17265): #7 BaseTapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:246:7)
E/flutter (17265): #8 PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:703:9)
E/flutter (17265): #9 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
E/flutter (17265): #10 PointerRouter._dispatchEventToRoutes. (package:flutter/src/gestures/pointer_router.dart:143:9)
E/flutter (17265): #11 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
E/flutter (17265): #12 PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18)
E/flutter (17265): #13 PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7)
E/flutter (17265): #14 GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:501:19)
E/flutter (17265): #15 GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:481:22)
E/flutter (17265): #16 RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:450:11)
E/flutter (17265): #17 GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:426:7)
E/flutter (17265): #18 GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:389:5)
E/flutter (17265): #19 GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:336:7)
E/flutter (17265): #20 GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:305:9)
E/flutter (17265): #21 _invoke1 (dart:ui/hooks.dart:328:13)
E/flutter (17265): #22 PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:442:7)
E/flutter (17265): #23 _dispatchPointerDataPacket (dart:ui/hooks.dart:262:31)
E/flutter (17265):

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to take an offensive programming approach. My experience with this project tells me that users don't really read the documentation, and instead they go straight to the issue tracker to complain when something isn't as they expect, so letting users provide both text and uri will lead to users thinking both can be shared at the same time, instead I prefer that the package enforces providing data correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand, in many scenarios, the content to be shared is not necessarily predetermined — it is decided at runtime or fetched from a server API. In such cases, both text and uri might be present. If I had to choose between (1) failing to share and (2) only sharing part of the content, I would definitely choose the latter. I believe our goal is to enable sharing. We should do everything we can to ensure the share succeeds, rather than blocking it just because the parameters don’t exactly meet our expectations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can leverage asser() to ensure the production environment while also restricting the development environment. I’m not sure if this would be better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, and if it was an existing API, I would definitely go with asserts instead. I'd say we merge it like this and see how it goes, and depending on the feedback we can move it back.

@StanleyCocos
Copy link
Contributor

@miquelbeltran Sorry, I just realized I didn’t submit that comment.

@miquelbeltran
Copy link
Member Author

miquelbeltran commented Apr 18, 2025

Thanks again @StanleyCocos for taking the time helping with this, your feedback was very appreciated!

As promised, today is merging day, I will prepare a release afterward (edit: on Monday, don't release on Friday)

@miquelbeltran miquelbeltran merged commit 0a19d46 into main Apr 18, 2025
21 of 23 checks passed
@miquelbeltran miquelbeltran deleted the 3403-share-plus-refactor branch April 18, 2025 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants